# Copyright (c) 2018 Red Hat, Inc. All rights reserved. This copyrighted
# material is made available to anyone wishing to use, modify, copy, or
# redistribute it subject to the terms and conditions of the GNU General Public
# License v.2 or later.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""Miscellaneous routines."""
import pathlib
import re
from textwrap import indent
from urllib.parse import urlparse

import yaml


class ActionNotFound(Exception):
    """Raised when an action is not found."""


def is_url(string):
    """Check if a string can be interpreted as a URL."""
    return bool(urlparse(string).scheme)


def format_exception_stack(exc):
    """
    Format an exception's context stack as a series of indented messages.

    Args:
        exc:    The exception to format the stack of.

    Returns:
        The formatted exception stack.
    """
    assert isinstance(exc, Exception)
    string = ""
    prefix = ""
    while True:
        summary = ": ".join(s for s in (type(exc).__name__, str(exc)) if s)
        string += indent(summary, prefix)
        if exc.__context__:
            string += ":\n"
            prefix += "  "
            exc = exc.__context__
        else:
            break
    return string


def attr_parentage(obj, attr):
    """
    Ascend object parentage.

    Ascend object parentage, yielding non-None values of the specified
    attribute for each.

    Args:
        obj:        The object to start ascending at, or None for no object.
                    Must have "parent" attribute with either the parent object
                    or None, as must all the parent objects thus linked.
        attr:       The name of the attribute to return non-None values of.

    Yields:
        Values of the specified attribute in order of parentage.
    """
    assert obj is None or hasattr(obj, "parent")
    assert isinstance(attr, str)
    while obj is not None:
        value = getattr(obj, attr)
        if value is not None:
            yield value
        obj = obj.parent


def raise_action_not_found(action, command):
    """Raise the ActionNotFound exception."""
    raise ActionNotFound(
        f'Action: "{action}" not found in command "{command}"'
    )


def raise_invalid_database(database):
    """Raise an exception for a wrong database."""
    raise Exception(
        f'"{database}" is not a database directory.\n'
        'Use the --db option to specify an alternative database directory.'
    )


# A dictionary of strings accepted as ternary argparse option values
ARGPARSE_TERNARY_DICT = {
    'false': False, 'no': False,
    'ignore': None,
    'true': True, 'yes': True
}


# A "metavar" string for a ternary argparse option
ARGPARSE_TERNARY_METAVAR = "{" + ",".join(ARGPARSE_TERNARY_DICT) + "}"


def argparse_ternary(string):
    """
    Parse an argparse ternary option value.

    Parse an argparse value string representing a ternary logic value as
    supplied to an argparse-handled command-line option.

    Args:
        string: The string to parse. Must be either "false"/"no" for False,
        "true"/"yes" for True, or "ignore" for None, ignoring the string case.

    Returns:
        The parsed True, False, or None value.
    """
    try:
        return ARGPARSE_TERNARY_DICT[string.lower()]
    except KeyError:
        raise ValueError from None


class YamlDumper(yaml.Dumper):  # pylint: disable=too-many-ancestors
    """YAML Dumper with list indenting and without aliases."""

    def __init__(self, *args, **kwargs):
        """Initialize the dumper."""
        super().__init__(*args, **kwargs)
        # Do not create aliases
        self.ignore_aliases = lambda *args: True

    def increase_indent(self, flow=False, indentless=False):
        """Increase indent."""
        return super().increase_indent(flow, False)


def unindent(text):
    """
    Unindent text.

    Unindent text by removing at most the number of spaces present in the
    first non-empty line from the beginning of every line. Differs from
    textwrap.dedent(), which removes common leading whitespace instead.

    Args:
        text:   The text to unindent.

    Returns:
        The unindented text.
    """
    assert isinstance(text, str)
    unindent_re = None
    unindented_lines = []
    for line in text.splitlines(keepends=True):
        if line and line != "\n":
            if not unindent_re:
                unindent_re = re.compile(
                    "^ {0," + str(re.match(" *", line).end()) + "}"
                )
            line = unindent_re.sub("", line)
        unindented_lines.append(line)
    return "".join(unindented_lines)


def read_file_list(file_path):
    """Read a list of files from the given file into a set."""
    return set(
        f for f in pathlib.Path(file_path).read_text('utf8').split('\n') if f
    )


def type_get_name(type_):
    """Get a fully-qualified name of a type, but omit "builtins"."""
    assert isinstance(type_, type)
    module = type_.__module__
    if module == "builtins":
        module = ""
    else:
        module += "."
    return module + type_.__qualname__


def get_type_name(value):
    """Get a fully-qualified type name of a value, but omit "builtins"."""
    return type_get_name(type(value))
